iT邦幫忙

1

[12 Project 學 Node.js] Project 4 : Node Blog System

  • 分享至 

  • xImage
  •  

Description:

  • 使用 monk (ORM to Mongodb), express-validator, multer, moment (form date & time)
  • 基本的Blog系統
  • 功能:single view with comment \ Add Post \ Add Comment \ Add Category
  • 沒有包含登入登出驗證系統,可以自己參考Proejct 3的做法加上去

安裝 nodemon

到這裡決定先安裝 nodemon,透過nodemon 啟動 Server 的話,只要 js 有更動,就會自動重啟
雖然講師重啟得很開心,但我有點懶得一直手動重啟
註:jade & css 除外,改這兩個不需要重啟Server,nodemon 也不會偵測到這個改動

使用 npm install 安裝
加上 --save-dev option 代表這個 dependencies 是給開發人員使用的,也會自動加到package.json
npm install --save-dev nodemon

安裝完打開package.json,在 scripts 區塊加上執行 nodemon 指令: "dev"

...
  "scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon ./bin/www"
  },
...

使用以下指定啟動,之後程式變更時就會自動重啟 Server
npm run dev

http://ithelp.ithome.com.tw/upload/images/20170301/20104222uVPmocUxcS.png


App & Module Setup

安裝express-generator globally
npm install -g express-generator

透過express建立新project目錄 4_nodeblog
express 4_nodeblog

修改package.json,加入要用的 dependencies

{
  "name": "4-nodeblog",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.16.0",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.0",
    "express": "~4.14.1",
    "jade": "~1.11.0",
    "morgan": "~1.7.0",
    "serve-favicon": "~2.3.2",
    "monk": "https://github.com/vccabral/monk.git",
    "connect-flash": "*",
    "express-session": "*",
    "express-validator": "*",
    "express-messages": "*",
    "multer": "*",
    "moment": "*",
    "mongodb": "*"
  },
  "devDependencies": {
    "nodemon": "^1.11.0"
  }
}

monk: 類似於mongoose,MongoDB ORM,這邊用monk是想提供多種練習,再去選擇自己喜歡哪一種
moment: javascript library,用來format 日期時間格式
其他 module 都和 nodeauth project 類似,就不再多說明

安裝 modules
npm install

修改 app.js,import module

var session = require('session');
var multer = require('multer');
var upload = multer({ dest: './public/images' })
var expressValidator = require('express-validator');

var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');

app.locals.moment = require('moment');

routing,讓 router 可以存取到 DB

// Make our db accessible to our router
app.use(function(req, res, next){
    req.db = db;
    next();
});

加入 connect-flash, validator, session middleware (從 project 3 copy過來)

// Connect-Flash
app.use(require('connect-flash')());
app.use(function (req, res, next) {
  res.locals.messages = require('express-messages')(req, res);
  next();
});

// validator
app.use(expressValidator({
  errorFormatter: function(param, msg, value) {
      var namespace = param.split('.')
      , root    = namespace.shift()
      , formParam = root;

    while(namespace.length) {
      formParam += '[' + namespace.shift() + ']';
    }
    return {
      param : formParam,
      msg   : msg,
      value : value
    };
  }
}));

// Handle Sessions
app.use(session({
    secret:'secret',
    saveUninitialized: true,
    resave: true
}));

Layout template

這次不使用bootstrap,只使用jade

layout.jade

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
   .container
    img.logo(src='/images/nodebloglogo.png')
    nav
     ul
      li
       a(href='/') Home
      li
       a(href='/posts/add') Add Post
      li
       a(href='/categories/add') Add Category
    block content
    footer
     p NodeBlog © 2017

這邊會需要一張 logo 圖,可以隨意放自己喜歡的圖,或是到這個免費建logo的網址做一個

把圖片放到 project 下的 public\images 中,檔名需與 layout.jade 中定義的一致 (nodebloglogo.png)

修改style.css,撰寫css樣式

body {
  font: 15px Helvetica, Arial, sans-serif;
  background: #f4f4f4;
  color: #666;
}
.logo {
    text-align: center;
    margin: auto;
    padding-bottom: 10px;
    display: block;
}
.container {
    width: 750px;
    border: 1px solid #ccc;
    margin: 20px auto;
    padding: 20px;
    border-top: #83cd39 3px solid;
}

.clr {
    clear: both;
}
ul {
    padding: 0;
    margin: 0;
}
h1,h2,h3,p {
    padding: 5px 0;
    margin-bottom: 0;
}
p {
    margin: 0;
}
a {
  color: #00B7FF;
}
nav {
    background: #404137;
    overflow: auto;
    height: 40px;
    padding: 20px 0 0 10px;
    font-size: 10px;
}
nav li {
    float: left;
    list-style: none;
}
nav a {
    padding: 10px;
    margin: 0 10px;
    color: #fff;
}
nav a.current, nav a:hover {
    background: #83cd29;
    color: #000;
}

除了圖片之外,其他樣式應該如下圖
http://ithelp.ithome.com.tw/upload/images/20170301/20104222kvEqITZnru.png

上面的配色可以隨意調配,分享兩個之前我自己有在用的網站:

Color Drop 這個網站提供多種顏色並排的比較,可以用來查看網站多種色彩的搭配效果
Paletton 也提供網站色彩搭配,而且附有一個大大的調色盤


首頁顯示貼文

在 Mongo Shell create nodeblog DB,並新增 categories 和 posts 兩個 collection

use nodeblog
db.createCollection('categories');
db.createCollection('posts');

新增兩筆資料,等下測試要用

db.posts.insert({title:"Blog Post One", category:"Technology", arthor:"yuki", body:"This is the bo dy", date:ISODate()});
db.posts.insert({title:"Blog Post Two", category:"Science", arthor:"grace", body:"This is the body ", date:ISODate()});

query posts collection,確認資料有塞進去
db.posts.find().pretty();

修改 routes\index.js,加入 mongo db module 及 HTTP GET request

var express = require('express');
var router = express.Router();

//mongo db
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');

/* GET home page. */
router.get('/', function(req, res, next) {
    var db = req.db;
    var posts = db.get('posts');
    posts.find({}, {}, function(err, posts){
        res.render('index', { posts: posts });
    });

});

module.exports = router;

修改 index.jade,如果有任何posts,把每個post列出來
title含有超連結,利用post的 _id 作為routing path

extends layout

block content
  if posts
   each posts, i in posts
    .post
     h1
      a(href='/posts/show/#{post._id}')
       =post.title

http://ithelp.ithome.com.tw/upload/images/20170301/20104222ek8WvOJhVj.png

修改 style.css
美化幾個地方

/* 分類 */
.meta{
    padding: 7px;
    border: 1px solid #ccc;
    background: #ccc;
    margin-bottom: 10px;
}
/* Read More 連結 */
a.more{
    display: block;
    width: 80px;
    background: #404137;
    color: #fff;
    padding: 10px;
    margin-top: 30px;
    text-decoration: none;
}
/* 貼文 */
.post{
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}
/* 貼文Title連結 */
.post h1 a{
    color: #666;
    text-decoration: none;
}

使用 Moment 加上貼文的分類、作者、日期和貼文內容,最後再加上 Read More 連結

extends layout

block content
  if posts
   each post, i in posts
    .post
     h1
      a(href='/posts/show/#{post._id}')
       =post.title
     p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
     =post.body
     a.more(href='/posts/show/#{post._id}') Read More

弄完之後應該長得像這樣
http://ithelp.ithome.com.tw/upload/images/20170301/20104222C6Z8WEbPTF.png


新增貼文功能

接下來要撰寫新增貼文的功能
修改 app.js 的 routing,把 users 改成 posts

...
var index = require('./routes/index');
var posts = require('./routes/posts');
...
app.use('/', index);
app.use('/posts', posts);
...

在 routes 下新增 posts.js,內容從 user.js copy過來,把user.js刪掉(這邊用不到)
修改成下面這樣:

var express = require('express');
var router = express.Router();

router.get('/add', function(req, res, next) {
    res.render('addpost', {
        'title': 'Add Post'
    });
});

module.exports = router;

接下來要為 posts 新增 view
在 view 下新增 addpost.jade,撰寫template

extends layout

extends layout

block content
    h1=title
    ul.errors
        if errors
            each error, i in errors
                li.alert.alert-danger #{error.msg}
    form(method='post', action='/posts/add', enctype="multipart/form-data")
        .form-group
            label Title:
            input.form-control(name='title', type='text')
        .form-group
            label Category:
            select.form-control(name='category')           
        .form-group
            label Body:
            textarea.form-control(name='body', id='body')
        .form-group
            label Main Image:
            input.form-control(name='mainimage', type='file')
        .form-group
            label Author:
            select.form-control(name='author')
                option(value='byakuinss') byakuinss
                option(value='yuki') yuki
        input.btn.btn-default(name='submit', type='submit', value='Save')

html 寫完了,再來修改 style.css,為 addpost.jade 加上css樣式

input, select, textarea{
    margin-bottom: 15px;
}

label{
    display: inline-block;
    width: 180px;
}

input[type='text'], select, textarea{
    padding: 3px;
    height: 20px;
    width: 200px;
    border: 1px #ccc solid;
}

select{
    height: 28px;
}

textarea{
    height: 70px;
    width: 400px;
}

接下來要將貼文存到DB,需要在 posts.js 加入 HTTP POST request
POST request 主要有以下幾個動作

  • 取得表單輸入值:
    req.body.{variable},variable 內容需要和 addpost.jade 中的 form-control(name='variable', ...) 相同
  • 檢查是否有上傳圖片、檢查必要欄位是否為空
  • 如果有任何 error 會回傳,若無問題,則使用 flash 顯示 "Post Added" message 並切換回首頁
//Require multer to handle image
var multer  = require('multer');
var upload = multer({ dest: './public/images' });
     ......
router.post('/add', upload.single('mainimage'), function(req, res, next) {
    //Get Form Values
    var title = req.body.title;
    var category = req.body.category;
    var body = req.body.body;
    var author = req.body.author;
    var date = new Date();

    //Check Image Upload
    if(req.file){
        var mainimage = req.file.filename;
    } else {
        var mainimage = 'no-image.jpg'
    }

    //Form Validation
    req.checkBody('title', 'Title field is required').notEmpty();
    req.checkBody('body', 'Body field is required').notEmpty();

    //Check Errors
    var errors = req.validationErrors();

    if(errors){
        res.render('addpost', {
            "errors": errors
        });
    } else {
        var posts = db.get('posts');
        posts.insert({
            "title": title,
            "body": body,
            "category": category,
            "date": date,
            "author": author,
            "mainimage": mainimage
        }, function(err, post){
            if(err) {
                res.send(err);
            } else {
                req.flash('success', 'Post Added');
                res.location('/');
                res.redirect('/');
            }
        });
    }
});
     ......

重啟 Server,試著新增一篇貼文,此時的 Category 沒有資料,這項先跳過不選
http://ithelp.ithome.com.tw/upload/images/20170301/20104222LQoYVEh0ED.png

新增完應該會看到剛剛新增的貼文出現在首頁
http://ithelp.ithome.com.tw/upload/images/20170301/20104222IM8mZyfdZM.png

現在來補足 Category 選單,先到 Mongo Shell在 categories collection 手動新增幾筆資料

db.categories.insert({name:'Technology'});
db.categories.insert({name:'Science'});
db.categories.insert({name:'Business'});

要讓 Category 選單從 categories collection 抓出資料,需要在 GET Add Post 頁面時加入DB連線,並將資料存到 categories
修改 posts.js 的 GET request 內容,將取得的DB資料存入 categories 參數

router.get('/add', function(req, res, next) {

    var categories = db.get('categories');

    categories.find({}, {}, function(err, categories){
        res.render('addpost', {
            'title': 'Add Post',
            'categories': categories
        });       
    });
});

接下來要從 categories 參數中取出值,並串連到 addpost.jade 的 category 選單
修改 addpost.jade,在 Category 選單下加入兩行,針對每一個 category,在選單中顯示

category.name
        ...
        .form-group
            label Category:
            select.form-control(name='category')
                each category, i in categories
                    option(value='#{category.name}') #{category.name}
        ...

在選單中可以看到所有 categories 了
http://ithelp.ithome.com.tw/upload/images/20170301/20104222mZLJ9wo2SI.png

新增一篇帶有 category 的貼文,測試成功
http://ithelp.ithome.com.tw/upload/images/20170301/20104222iGq3cgk64F.png


文字編輯器

在新增貼文時,如果有文字編輯器,貼文的內容就可以有更多變化
這邊選擇的是 CKEditor,因為比較容易Setup

CKEditor官網下載 Standard Package
下載後解壓會產生ckeditor folder,把整個 folder 複製到 project folder 的 public 資料夾下

修改 addpost.jade,在最下方加入script,import ckeditor.js,並且用來取代原本 body 區塊

...
        input.btn.btn-default(name='submit', type='submit', value='Save')
        script(src='/ckeditor/ckeditor.js')
        script
            | CKEDITOR.replace('body');
...

重新進入 Add Post 頁面,就會看到原本 body 區塊的 textarea 已經變成文字編輯器囉
http://ithelp.ithome.com.tw/upload/images/20170301/20104222E06aBu00Pv.png


新增Category功能

修改 app.js,加入 categories routing

...
var index = require('./routes/index');
var posts = require('./routes/posts');
var categories = require('./routes/categories')
...
app.use('/', index);
app.use('/posts', posts);
app.use('/categories', categories);
...

新增兩個檔案: addcategory.jade \ categories.js
複製 addpost.jade 到 addcategory.jade,只留下一個 text 和 button,如下

extends layout

block content
    h1=title
    ul.errors
        if errors
            each error, i in errors
                li.alert.alert-danger #{error.msg}
    form(method='post', action='/categories/add')
        .form-group
            label Name:
            input.form-control(name='name', type='text')
        input.btn.btn-default(name='submit', type='submit', value='Save')

同樣複製 posts.js 到 categories.js,只留下需要的 module,修改 GET \ POST request 內容

var express = require('express');
var router = express.Router();
var mongo = require('mongodb');
var db = require('monk')('localhost/nodeblog');

router.get('/add', function(req, res, next) {
    res.render('addcategory', {
        'title': 'Add Category'
    });   
});

router.post('/add', function(req, res, next) {
    //Get Form Values
    var name = req.body.name;

    //Form Validation
    req.checkBody('name', 'Name field is required').notEmpty();

    //Check Errors
    var errors = req.validationErrors();

    if(errors){
        res.render('addcategories', {
            "errors": errors
        });
    } else {
        var categories = db.get('categories');
        categories.insert({
            "name": name
        }, function(err, category){
            if(err) {
                res.send(err);
            } else {
                req.flash('success', 'Category Added');
                res.location('/');
                res.redirect('/');
            }
        });
    }
});

module.exports = router;

新增一個 Category 測試,新增完再到 Add Post,新category已經出現在選單中了
http://ithelp.ithome.com.tw/upload/images/20170301/20104222Hfrqc4K1J7.pnghttp://ithelp.ithome.com.tw/upload/images/20170301/20104222tEvTK1TReu.png


縮短文字內容 (truncate text) & 顯示上傳圖片

把之前測試的新增貼文都刪除
db.posts.remove("");

修改layout.jade,加入success message

    ...
      li
       a(href='/categories/add') Add Category
    != messages()
    block content
    ...

修改style.css,加入 success message 顯示的css樣式

...
ul.success li{
    padding: 15px;
    margin-top: 10px;
    margin-bottom: 20px;
    border: 1px solid transparent;
    border-radius: 4px;
    color: #3c763d;
    background-color: #dff0d8;
    border-color: #d6e9c6;
    list-style: none;
}

接下來加入truncate text效果

新增一篇很長的貼文
http://ithelp.ithome.com.tw/upload/images/20170301/20104222ywTU6wvfSR.png

修改app.js,加入一個新function: truncateText

...
app.locals.moment = require('moment');

app.locals.truncateText = function(text, length){
  var truncateText = text.substring(0, length);
  return truncateText;
}
...

修改index.jade,將=post.body改成 !=truncateText(post.body,400)

  ......
     p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
     !=truncateText(post.body,400)
     a.more(href='/posts/show/#{post._id}')
  ...... 

重啟server,可以看到過長的貼文被截掉了
http://ithelp.ithome.com.tw/upload/images/20170301/201042222w8t4lHytE.png

接下來要把 image 加入貼文
先確認新增貼文時上傳的圖片有在 public\images中
http://ithelp.ithome.com.tw/upload/images/20170301/20104222ps4bvRfAyx.png

修改 index.jade,在 p.meta 下方加入圖片label (可以依自己喜好隨意放)

     ...
     p.meta Posted in #{post.category} by #{post.author} on #{moment(post.date).format("MM-DD-YYYY")}
     img(src='/images/#{post.mainimage}')
     !=truncateText(post.body,400)
     ...

修改 style.css加上圖片css樣式,因為上傳的圖片可能大小不一,我希望圖片都是隨視窗改變大小

.post img {
    width: 100%;
}

重新整理網頁,圖片就出來囉
http://ithelp.ithome.com.tw/upload/images/20170301/20104222wvomH12bt4.png

註:沒看到圖片怎麼辦

  • 檢查 app.js 和 post.js 的 upload 路徑是否一致
  • 檢查 public\images 中是否有應該顯示的圖片,如果沒有,可能是 post.js 的路徑沒改到或寫錯

以Category View檢視貼文

需要新增一個新頁面,用Category當作query條件show出對應的貼文
修改categories.js,加入新的 route,將要query的category條件放入 posts.find({query_condition}, {}, function ...)

router.get('/show/:category', function(req, res, next) {
    var posts = db.get('posts');

    posts.find({category: req.params.category}, {}, function(err, posts){
        res.render('index', {  //切換回index以顯示貼文
            'title': req.params.category, //標題為query的category
            'posts': posts  //顯示query到的貼文
        });       
    });   
});

修改index.jade,將category文字改成連結,點選連結就會顯示出該分類的所有貼文

    ....
     p.meta Posted in
      a(href='/categories/show/#{post.category}') #{post.category}
      by #{post.author}
      on #{moment(post.date).format("MM-DD-YYYY")}
     img(src='/images/#{post.mainimage}')
    ...

重啟 Server 測試,看到 Category 文字帶有連結,且點選後只出現該分類的貼文
http://ithelp.ithome.com.tw/upload/images/20170301/20104222N6gyx62lDG.png


檢視單篇貼文內容

目前點選 Read More 按鈕,還無法顯示單篇貼文內容
和分類顯示相同,需要為單篇貼文顯示建立新的 route

修改 posts.js,透過 id 找到對應的貼文並以 show view 顯示

...
router.get('/show/:id', function(req, res, next) {

    var posts = db.get('posts');

    posts.findById(req.params.id, function(err, post){
        res.render('show', {
            'post': post
        });       
    });
});
...

新增 views\show.jade 檔案,內容從 index.jade copy 過來修改
因為show只需要顯示單篇貼文,所以去掉 loop, title 連結,truncate text改成顯示完整body

extends layout

block content
    .post
     h1=post.title
     p.meta Posted in
      a(href='/categories/show/#{post.category}') #{post.category}
      by #{post.author}
      on #{moment(post.date).format("MM-DD-YYYY")}
     img(src='/images/#{post.mainimage}')
     !=post.body

重啟 Server,點選 Read More 就可以顯示完整的單篇貼文了
http://ithelp.ithome.com.tw/upload/images/20170301/20104222ogHgfSrYWy.png


在單篇貼文中加入 comments 區塊

修改 show.js,從 post.body 往下新增 comments 區塊
如果目前有任何 comments就會顯示,無論是否有已存在 comments 都會顯示新增comment的表單

     ...
     img(src='/images/#{post.mainimage}')
     !=post.body
     br
     hr
     if post.comments
      h3 Comments
      each comment, i in post.comments
       .comment
        p.comment-name #{comment.name}
        p.comment-body #{comment.body}
      br
    h3 Add Comment
    if errors
     ul.errors
      each error, i in errors
       li.alert.alert-danger #{error.msg}
    form.comment-form(method='post', action='/posts/addcomment')
     input(name='postid', type='hidden', value='#{post._id}')
     .form-group
      label Name
      input.form-control(type='text', name='name')
     .form-group
      label Email
      input.form-control(type='text', name='email')     
     .form-group
      label Body
      textarea.form-control(type='text', name='body')
     br
     input.btn.btn-default(type='submit', name='submit', value='Add Comment')

http://ithelp.ithome.com.tw/upload/images/20170301/2010422241T43bS3Y6.png

接下來為 Add Comment 按鈕撰寫 POST request
修改 post.js,加入新的 POST route (從 add post copy)

router.post('/addcomment', function(req, res, next) {  //修改名稱為addcomment
    //Get Form Values
    var name = req.body.name;
    var email = req.body.email;
    var body = req.body.body;
    var postid = req.body.postid;
    var commentdate = new Date();

    //Form Validation
    req.checkBody('name', 'Name field is required').notEmpty();
    req.checkBody('email', 'Email field is required but never displayed').notEmpty();
    req.checkBody('email', 'Email field is not formatted properly').isEmail();       
    req.checkBody('body', 'Body field is required').notEmpty();

    //Check Errors
    var errors = req.validationErrors();

    if(errors){  //如果add comment有error仍要顯示貼文
        var posts = db.get('posts');  
        posts.findById(postid, function(err, post){
            res.render('show', {
                "errors": errors,
                "post": post
            });           
        });
    } else { //如果沒有錯誤,將comment內容update到該篇貼文的comments欄位
        var comment = {
            "name": name,
            "email": email,
            "body": body,
            "commentdate": commentdate
        }

        var posts = db.get('posts');
        
        posts.update({
            "_id": postid
        }, {
            $push: {
                "comments": comment
            }
        }, function(err, doc){  //都沒有問題就切換到單篇貼文頁面
            if(err){
                throw err;
            } else {
                req.flash('success', 'Comment Added');
                res.location('/posts/show/'+postid);
                res.redirect('/posts/show/'+postid);
            }
        });
    }
});

重啟 Server,新增一篇 comment 測試看看,網頁會自動切換,也會顯示成功新增comment的訊息
http://ithelp.ithome.com.tw/upload/images/20170301/201042221PsgGNNstm.pnghttp://ithelp.ithome.com.tw/upload/images/20170301/20104222cPl8ArxYow.png


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言